﻿namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    internal class AssemblyInformation
    {
        private readonly IList<string> assemblyReferences = new List<string>();
        private readonly IDictionary<AssemblyBuilderLanguage, CompileHelper> compileHelpers = new Dictionary<AssemblyBuilderLanguage, CompileHelper>();

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="AssemblyInformation"/> class.
        /// </summary>
        /// <param name="assemblyBuilderAttributes">The attributes to build the assembly from.</param>
        public AssemblyInformation(Type type, AssemblyBuilderAttributeCollector assemblyBuilderAttributes)
        {
            this.ProcessAllAttributes(type, assemblyBuilderAttributes);
        }
        #endregion Constructors

        #region Public Methods
        public string GetAssemblyFileNameForMethod(MethodBase method, bool debugMode = true)
        {
            var compileHelper = this.GetCompileHelperForMethod(method);
            return debugMode ? compileHelper.DebugAssemblyFileName : compileHelper.ReleaseAssemblyFileName;
        }

        public string GetAssemblyFileNameForMethod(MethodBase method, AssemblyBuilderLanguage language, bool debugMode = true)
        {
            var compileHelper = this.GetCompileHelperForMethod(method, language);
            return debugMode ? compileHelper.DebugAssemblyFileName : compileHelper.ReleaseAssemblyFileName;
        }

        public Assembly GetAssemblyForMethod(MethodBase method, bool debugMode = true)
        {
            var compileHelper = this.GetCompileHelperForMethod(method);
            return debugMode ? compileHelper.DebugAssembly : compileHelper.ReleaseAssembly;
        }

        public Assembly GetAssemblyForMethod(MethodBase method, AssemblyBuilderLanguage language, bool debugMode = true)
        {
            var compileHelper = this.GetCompileHelperForMethod(method, language);
            return debugMode ? compileHelper.DebugAssembly : compileHelper.ReleaseAssembly;
        }

        /// <summary>
        /// Gets the output directory that assemblies will be built in.
        /// </summary>
        /// <param name="method">The test method.</param>
        /// <returns>
        /// The output directory that assemblies will be built in.
        /// </returns>
        public DirectoryInfo GetOutputDirectory(MethodBase method)
        {
            var directoryName = AssemblyBuilderDirectoryAttribute.GetDirectoryName(method.DeclaringType);
            var outputPath = AssemblyReferences.GetReferencePath(directoryName);

            return new DirectoryInfo(outputPath);
        }
        #endregion Public Methods

        #region Private Methods
        private CompileHelper GetCompileHelperForMethod(MethodBase method)
        {
            var language = GetLanguageFromAttributes(method);
            if (language == AssemblyBuilderLanguage.Unknown)
            {
                language = GetLanguageFromAttributes(method.DeclaringType);
                if (language == AssemblyBuilderLanguage.Unknown)
                {
                    throw new InvalidOperationException("The language must be able to be determined at the method or type level.");
                }
            }

            return GetCompileHelperForMethod(method, language);
        }

        private CompileHelper GetCompileHelperForMethod(MethodBase method, AssemblyBuilderLanguage language)
        {
            var compileHelper = this.compileHelpers[language];

            var directoryName = AssemblyBuilderDirectoryAttribute.GetDirectoryName(method.DeclaringType);
            var outputPath = AssemblyReferences.GetReferencePath(directoryName);
            if (outputPath != null)
            {
                compileHelper.SetOutputPath(outputPath);
            }

            var assemblyName = AssemblyBuilderNameAttribute.GetAssemblyName(method.DeclaringType);
            if (assemblyName != null)
            {
                if (outputPath == null)
                {
                    throw new InvalidOperationException("Output path must be specified.");
                }

                compileHelper.SetAssemblyFileName(Path.Combine(outputPath, assemblyName));
            }

            return compileHelper;
        }

        private static AssemblyBuilderLanguage GetLanguageFromAttributes(MemberInfo member)
        {
            var codeAttribute = AssemblyBuilderAttributeCollector.GetCodeAttributes(member)
                .FirstOrDefault();
            if (codeAttribute != null)
            {
                return codeAttribute.Language;
            }

            var fileAttribute = AssemblyBuilderAttributeCollector.GetFilesAttribute(member);
            if (fileAttribute != null)
            {
                return
                    (from fileName in fileAttribute.FileNames
                     let language = GetLanguageFromFileName(fileName)
                     where language != AssemblyBuilderLanguage.Unknown
                     select language)
                    .FirstOrDefault();
            }

            return AssemblyBuilderLanguage.Unknown;
        }

        private static AssemblyBuilderLanguage GetLanguageFromFileName(string fileName)
        {
            var extension = Path.GetExtension(fileName);
            if (extension.Equals(".cs", StringComparison.OrdinalIgnoreCase))
            {
                return AssemblyBuilderLanguage.CSharp;
            }
            else if (extension.Equals(".vb", StringComparison.OrdinalIgnoreCase))
            {
                return AssemblyBuilderLanguage.VB;
            }
            else
            {
                return AssemblyBuilderLanguage.Unknown;
            }
        }

        private static void AddFileToCompileHelper(CompileHelper compileHelper, string path, string file)
        {
            var fileName = Path.Combine(path, file);
            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException("The file could not be found. You probably need to mark the file in your project BuildAction = Content and Copy to Output Directory = Copy if newer.");
            }

            compileHelper.AddSource(File.ReadAllText(fileName));
        }

        private void AddFilesToCompileHelper(IEnumerable<string> fileNames)
        {
            var path = Path.GetDirectoryName(typeof(AssemblyBuilder).Assembly.Location);

            foreach (var fileName in fileNames)
            {
                var language = GetLanguageFromFileName(fileName);
                if (language == AssemblyBuilderLanguage.Unknown)
                {
                    throw new InvalidOperationException("Only 'cs', 'vb' extensions are valid.");
                }
                else
                {
                    CompileHelper compileHelper = this.GetCompileHelperForLanguage(language);
                    AddFileToCompileHelper(compileHelper, path, fileName);
                }
            }
        }

        private string GetFullReferencePath(Type type, string reference)
        {
            var extension = Path.GetExtension(reference);
            switch (extension)
            {
                case ".dll":
                case ".exe":
                    return AssemblyReferences.GetReferenceFullPath(type, reference);

                default:
                    // Assume file names without an extension are in the GAC.
                    return reference + ".dll";
            }
        }

        private void ProcessAllAttributes(Type type, AssemblyBuilderAttributeCollector assemblyBuilderAttributes)
        {
            this.AddAssemblyFilesToCompile(assemblyBuilderAttributes.FileAttributes);
            this.AddAssemblyCodeToCompile(assemblyBuilderAttributes.CodeAttributes);
            this.AddAssemblyResourcesToCompile(assemblyBuilderAttributes.ResourceAttributes);

            // Set the assembly references for all the different compilerHelpers.
            foreach (var compileHelper in this.compileHelpers.Values)
            {
                foreach (var assemblyReferenceAttribute in assemblyBuilderAttributes.ReferenceAttributes)
                {
                    foreach (var assemblyReference in assemblyReferenceAttribute.AssemblyReferences)
                    {
                        var fullReferencePath = this.GetFullReferencePath(type, assemblyReference);
                        compileHelper.AddAssemblyReference(fullReferencePath);
                    }
                }
            }
        }

        private void AddAssemblyResourcesToCompile(IEnumerable<AssemblyBuilderResourceAttribute> resourceAttributes)
        {
            var resourcesGroupedByName =
                from resourceAttribute in resourceAttributes
                group resourceAttribute by resourceAttribute.ResourceName into resourceGroup
                select resourceGroup;
            foreach (var resourceGroup in resourcesGroupedByName)
            {
                foreach (var compileHelper in this.compileHelpers.Values)
                {
                    compileHelper.AddEmbeddedResource(
                        resourceGroup.Key,
                        writer =>
                        {
                            foreach (var resourceAttribute in resourceGroup)
                            {
                                var resourceItems = resourceAttribute.ResourceItems;
                                for (int i = 0; i < resourceItems.Length; i += 2)
                                {
                                    writer.AddResource(resourceItems[i], resourceItems[i + 1]);
                                }
                            }
                        });
                }
            }
        }

        private void AddAssemblyFilesToCompile(IEnumerable<AssemblyBuilderFilesAttribute> fileAttributes)
        {
            foreach (var fileAttribute in fileAttributes)
            {
                this.AddFilesToCompileHelper(fileAttribute.FileNames);
            }
        }

        private void AddAssemblyCodeToCompile(IEnumerable<AssemblyBuilderCodeAttribute> codeAttributes)
        {
            foreach (var codeAttribute in codeAttributes)
            {
                var compileHelper = this.GetCompileHelperForLanguage(codeAttribute.Language);
                compileHelper.AddSource(codeAttribute.Code);
            }
        }

        private CompileHelper GetCompileHelperForLanguage(AssemblyBuilderLanguage language)
        {
            CompileHelper compileHelper;
            if (!this.compileHelpers.TryGetValue(language, out compileHelper))
            {
                if (language == AssemblyBuilderLanguage.CSharp)
                {
                    compileHelper = new CompileHelper().SetCodeProvider("cs");
                }
                else if (language == AssemblyBuilderLanguage.VB)
                {
                    compileHelper = new CompileHelper().SetCodeProvider("vb");
                }

                if (compileHelper == null)
                {
                    throw new InvalidOperationException("A code language must be specified for the test.");
                }

                this.compileHelpers.Add(language, compileHelper);
            }

            return compileHelper;
        }
        #endregion Private Methods
    }
}